﻿using System;
using System.Collections.Generic;
using System.Linq;
using kunukn.RecommendationEngine.AreaRecommendation.Code.Entities;
using kunukn.RecommendationEngine.AreaRecommendation.Code.Helpers;


namespace kunukn.RecommendationEngine.AreaRecommendation.Code.Engine
{
    /// <summary>
    /// Author: Kunuk Nykjaer
    /// The matrix must be a square, not rectangle
    /// </summary>
    [Serializable]
    public class Item2Item
    {        
        private Item[] _items;
        private int[][] _data;
        
        public Item2Item()
        {
            InitItems(SessionHelper.GetItems());
        }
        public Item2Item(List<Item>  items)
        {
            InitItems(items);
        }

        public void Clear()
        {
            int count = _data.Length;
            _data = new int[count][];
            for (int i = 0; i < count; i++)
            {
                _data[i] = new int[count];
            }
                
        }

        /// <summary>
        /// Add viewed item to item information to matrix
        /// 
        /// e.g. if user has viewed in order A,B,C and then views D. 
        /// The item to item information c->d is saved to the matrix
        /// </summary>
        /// <param name="item"></param>
        public void AddViewed(Item item)
        {
            var list = SessionHelper.GetViewedItems();
            if (list.Count > 0)
            {
                var lastViewed = list.Last();
                int i0 = lastViewed.GetItemIndex();
                int i1 = item.GetItemIndex();

                _data[i0][i1] = _data[i0][i1] + 1;

                // Don't count same item view twice, e.g. viewed A, then viewed A again is only counted once                
                if (i0 != i1)
                {
                    // If direction matters, then outcomment below line
                    // e.g. if A then B is viewed and should be the same as B then A is viewed then use below line  
                    // Bijection or not
                    _data[i1][i0] = _data[i1][i0] + 1;
                }
            }
        }

        public int GetCount()
        {
            return _items.Length;
        }

        public Item GetItemByIndex(int i)
        {
            if (i < 0 || i >= _items.Length)
            {
                throw new ApplicationException(string.Format("GetItemByIndex() index invalid: {0}", i));
            }
                
            return _items[i];
        }

        public int[] GetVector(Item item)
        {
            return GetVectorSelfIncluded(item);
        }

        protected int[] GetVectorSelfIncluded(Item item)
        {
            if (!item.IsValid())
            {
                throw new ApplicationException(string.Format("GetVectorSelfIncluded() - Item is not valid: {0}", item == null ? "null" : item.ToString()));
            }                
            
            return _data[item.GetItemIndex()];            
        }
        protected int[] GetVectorSelfExcluded(Item item)
        {
            var index = item.GetItemIndex();
            int[] vector = GetVectorSelfIncluded(item);                                    
            var vectorSelfExcluded = vector.Where((t, i) => i != index).ToArray();
            return vectorSelfExcluded;
        }

        public int[] GetVectorY(Item item)
        {
            if (!item.IsValid())
            {
                throw new ApplicationException(string.Format("GetVectorY() - Item is not valid: {0}", item == null ? "null" : item.ToString()));
            }
                
            int index = item.GetItemIndex();
            int[] rank = new int[_data.Length];
            for (int i = 0; i < _data.Length; i++)
            {
                rank[i] = _data[i][index];
            }
                
            return rank;
        }

        /// <summary>
        /// Setup the Item 2 Item array
        /// </summary>
        /// <param name="items"></param>
        protected void InitItems(List<Item> items)
        {
            _items = items.ToArray();
            int count = items.Count;

            _data = new int[count][];
            for (int i = 0; i < count; i++)
            {
                _data[i] = new int[count];
            }                
        }

        /// <summary>
        /// Take sorted associated items except the current item
        /// O(nlogn) where n is items
        /// </summary>
        /// <param name="currentItem"></param>
        /// <returns></returns>
        public List<ItemCount> GetAssociated(Item currentItem)
        {
            if (!currentItem.IsValid())
            {
                return new List<ItemCount>();
            }                

            int currentIndex = currentItem.GetItemIndex();
            int[] arr = _data[currentIndex];

            // Collect all and return the associated
            var ics = new List<ItemCount>();
            for (int i = 0; i < arr.Length; i++)
            {
                // skip current item
                if (currentIndex == i)
                {
                    continue;
                }
                    
                int count = arr[i];
                Item item = _items[i];
                
                ics.Add(new ItemCount(item) {Count = count });
            }
            return ics;
        }
        public List<ItemCount> GetAssociatedSorted(Item currentItem)
        {
            var list = GetAssociated(currentItem);
            return list.OrderByDescending(a => a.Count).ToList();
        }

        public Item[] GetItems()
        {
            return _items;
        }
        public int[][] GetMatrix()
        {
            return _data;
        }

        private const int Round = 3;
        public List<SimilarityResult> GetCosineList(Item x)
        {
            int currentIndex = x.GetItemIndex();
            Item2Item i2i = SessionHelper.GetItem2Item();
            var list = new List<SimilarityResult>();
            int[] a = i2i.GetVector(x);
            
            int count = i2i.GetCount();
            for (int i = 0; i < count; i++)
            {
                if (i == currentIndex)
                {
                    continue; //don't apply fn on same item
                }
                    
                Item itemb = _items[i];
                int[] b = i2i.GetVector(itemb);
                
                double value = Math.Round(MathUtil.CosineSimilarity(a, b), Round);
                list.Add(new SimilarityResult(x) {ItemOther = itemb, Measure = value });
            }
            return list;
        }

        public List<SimilarityResult> GetJaccardIndexList(Item x)
        {
            int currentIndex = x.GetItemIndex();
            Item2Item i2i = SessionHelper.GetItem2Item();
            var list = new List<SimilarityResult>();
            int[] a = i2i.GetVector(x);

            int count = i2i.GetCount();
            for (int i = 0; i < count; i++)
            {
                if (i == currentIndex)
                {
                    continue; //don't apply fn on same item
                }
                    
                Item itemb = _items[i];
                int[] b = i2i.GetVector(itemb);

                double value = Math.Round(MathUtil.JaccardIndexSimilarity(a, b), Round);
                list.Add(new SimilarityResult(x) { ItemOther = itemb, Measure = value });
            }
            return list;
        }

    }
}